package flockingGA;

import jnibwapi.JNIBWAPI;
import jnibwapi.model.Unit;
import jnibwapi.types.OrderType;
import jnibwapi.types.UnitType;
import jnibwapi.types.WeaponType;


import math.geom2d.AffineTransform2D;
import math.geom2d.Point2D;
import math.geom2d.Vector2D;
import math.geom2d.polygon.Rectangle2D;
import math.geom2d.polygon.SimplePolygon2D;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;

/**
 * Created by Stefan Rudolph on 18.02.14.
 */
public class Marine {

    final private JNIBWAPI bwapi;
    private final HashSet<Unit> enemyUnits;
    final private Unit unit;
    private int id;
    
    
    public Marine(Unit unit, JNIBWAPI bwapi, HashSet<Unit> enemyUnits, int id) {
        this.unit = unit;
        this.bwapi = bwapi;
        this.enemyUnits = enemyUnits;
        this.id = id;
    }
    
    public void step() {
        Unit target = getClosestEnemy();
        if( target == null )
        	return;
        
        int attackOrderID = OrderType.OrderTypes.AttackUnit.getID();
        
        if( unit.getOrderID() == attackOrderID || unit.isAttackFrame() || unit.isStartingAttack() || unit.isAttacking() )
        	return;
        
        int sightRange = bwapi.getUnitType( UnitType.UnitTypes.Terran_Marine.getID() ).getSightRange();
        int attackRange = bwapi.getWeaponType(WeaponType.WeaponTypes.Gauss_Rifle.getID()).getMaxRange();
        
        double distance = getDistance(target);

    	// 4)
    	if( distance <= (attackRange + 32*1) )
    	{
			//System.out.println("Attacking");
			bwapi.attack(unit.getID(), target.getID());
    	}	
    	else
    	{
    		//System.out.println("Move true");
    		move(target, (distance < sightRange) );        		
    	}
    }
    
    
    private void move(Unit target, boolean seeEnemy){
    	// check if we need to separate from other units
    	double[] sep = separate();
    	double tx,ty;
    	if( sep[0] < 0.0001 && sep[0] > 0.0001 && sep[1] < 0.0001 && sep[1] > 0.0001) 
    	{
    		tx = sep[0] * w_r2;
    		ty = sep[1] * w_r2;
    		//System.out.println("SEP: "+tx+" , "+ty);
    	}
    	else
    	{
    		double[] t = { target.getX()-unit.getX(), target.getY()-unit.getY() };
    		double[] r3 = rule3();
    		double[] r4 = rule4();
    		
    		double w = seeEnemy ? this.w_r1_b : this.w_r1_a;
    		tx = t[0] * w + r3[0] * this.w_r3 + r4[0] * this.w_r4;
        	ty = t[1] * w + r3[1] * this.w_r3 + r4[1] * this.w_r4;
        	//System.out.println("ELSE: "+tx+" , "+ty);
    	}
    	// normalize
    	tx += unit.getX();
    	ty += unit.getY();
    	bwapi.move(unit.getID(), (int)tx, (int)ty);
    }

    private Unit getClosestEnemy() {
        Unit result = null;
        double minDistance = Double.POSITIVE_INFINITY;
        for (Unit enemy : enemyUnits) {
            double distance = getDistance(enemy);
            if (distance < minDistance) {
                minDistance = distance;
                result = enemy;
            }
        }

        return result;
    }
    
    // Rule 2)
    private double[] separate(){
    	double[] result = new double[2];
    	
    	List<Unit> units = getNeighbors(this.r_sep);
    	for( Unit u: units )
    	{
    		result[0] += (u.getX() - unit.getX());
    		result[1] += (u.getY() - unit.getY());
    	}
    	
    	result[0] *= -1;
    	result[1] *= -1;
    	
    	return result;
    }
    
    // Rule 3)
    private double[] rule3()
    {
    	HashMap< Integer, ArrayList<Unit> > columnUnits = getColumnUnits();
    	
    	ArrayList<Unit> maxU = null;
    	double maxVal = 0;
    	for( Entry<Integer,ArrayList<Unit> > units: columnUnits.entrySet() )
    	{
    		for( Unit u : units.getValue() )
    		{
    			double[] c = cohesion(u);
        		c[0] += unit.getX();
        		c[1] += unit.getY();
        		double len = Math.sqrt( c[0]*c[0] + c[1]*c[1] );
        		double val = units.getValue().size() / len;
        		if( maxU == null || maxVal < val )
        		{
        			maxU = units.getValue();
        			maxVal = val;
        		}
    		}
    	}
    	
    	double[] r1 = cohesion(maxU);
    	double[] r2 = separate(this.r_col_sep);
    	
    	return new double[]{ r1[0]+r2[0], r1[1]+r2[1] };
    }
    
    // Rule 4)
    private double[] rule4()
    {
    	HashMap< Integer, ArrayList<Unit> > lineUnits = getLineUnits();
    	
    	ArrayList<Unit> maxU = null;
    	double maxVal = 0;
    	for( Entry<Integer,ArrayList<Unit> > units: lineUnits.entrySet() )
    	{
    		for( Unit u : units.getValue() )
    		{
    			double[] c = cohesion(u);
        		c[0] += unit.getX();
        		c[1] += unit.getY();
        		double len = Math.sqrt( c[0]*c[0] + c[1]*c[1] );
        		double val = units.getValue().size() / len;
        		if( maxU == null || maxVal < val )
        		{
        			maxU = units.getValue();
        			maxVal = val;
        		}
    		}
    	}
    	
    	double[] r1 = cohesion(maxU);
    	double[] r2 = separate(this.r_lin_sep);
    	
    	return new double[]{ r1[0]+r2[0], r1[1]+r2[1] };
    }
    
    private HashMap< Integer, ArrayList<Unit> > getColumnUnits()
    {
    	HashMap< Integer, ArrayList<Unit> > result = new HashMap< Integer, ArrayList<Unit> >();
    	
    	AffineTransform2D at = AffineTransform2D.createTranslation( new Vector2D(this.r_col,0) );
    	
    	Rectangle2D r = new Rectangle2D(-this.r_sig, -this.r_sig,this.r_lin,this.r_sig*2);
    	SimplePolygon2D sp = r.transform(AffineTransform2D.createIdentity());
    	// count columns
    	int columnCount = (int)Math.ceil((this.r_sig*2) / this.r_col);
    	for( int i = 0; i < columnCount; ++i )
    	{
    		ArrayList<Unit> temp = new ArrayList<Unit>();
    		for( Unit u: getNeighbors(this.r_sig) )
    		{
    			Point2D p = new Point2D(u.getX()-unit.getX(), u.getY()-unit.getY());
    			p.rotate(-(unit.getAngle()*Unit.TO_DEGREES));
    			
    			if( sp.contains(p) )
				{
    				temp.add(u);
				}
    		}
    		sp = sp.transform(at);
    		result.put(i, temp);
    	}
    	return result;
    }
    
    private HashMap< Integer, ArrayList<Unit> > getLineUnits()
    {
    	HashMap< Integer, ArrayList<Unit> > result = new HashMap< Integer, ArrayList<Unit> >();
    	
    	AffineTransform2D at = AffineTransform2D.createTranslation( new Vector2D(0,this.r_col) );
    	
    	Rectangle2D r = new Rectangle2D(-this.r_sig, -this.r_sig,this.r_sig*2,this.r_col);
    	SimplePolygon2D sp = r.transform(AffineTransform2D.createIdentity());
    	// count columns
    	int columnCount = (int)Math.ceil((this.r_sig*2) / this.r_col);
    	for( int i = 0; i < columnCount; ++i )
    	{
    		ArrayList<Unit> temp = new ArrayList<Unit>();
    		for( Unit u: getNeighbors(this.r_sig) )
    		{
    			Point2D p = new Point2D(u.getX()-unit.getX(), u.getY()-unit.getY());
    			p.rotate(-(unit.getAngle()*Unit.TO_DEGREES));
    			
    			if( sp.contains(p) )
				{
    				temp.add(u);
				}
    		}
    		sp = sp.transform(at);
    		result.put(i, temp);
    	}
    	return result;
    }
    
    private double[] separate(double radius){
    	double[] result =  {0,0};
    	
    	List<Unit> units = getNeighbors(radius);
    	if( units.size() == 0 )
    		return result;
    	
    	for( Unit u: units )
    	{
    		result[0] += (u.getX() - unit.getX());
    		result[1] += (u.getY() - unit.getY());
    	}
    	
    	result[0] *= -1;
    	result[1] *= -1;
    	
    	return result;
    }
    
    private double[] cohesion(Unit s) {
    	double[] result = new double[2];
    	
    	List<Unit> units = getNeighbors(this.r_sig);
    	for( Unit u: units )
    	{
    		if( s.getID() == u.getID() )
    			continue;
    		
    		result[0] += u.getX();
    		result[1] += u.getY();
    	}
    	result[0] /= units.size();
    	result[1] /= units.size();
    	
    	result[0] -= s.getX();
    	result[1] -= s.getY();
    	
    	return result;
    }
    
    private double[] cohesion(ArrayList<Unit> units) {
    	double[] result = new double[2];
    	if( units == null )
    		return result;
    	
    	for( Unit u: units )
    	{
    		if( unit.getID() == u.getID() )
    			continue;
    		
    		result[0] += u.getX();
    		result[1] += u.getY();
    	}
    	result[0] /= units.size();
    	result[1] /= units.size();
    	
    	result[0] -= unit.getX();
    	result[1] -= unit.getY();
    	
    	return result;
    }
    
    // Rule #3
    private double[] alignment() {
    	double[] result = new double[2];
    	
    	List<Unit> units = bwapi.getAlliedUnits();
    	for( Unit u: units )
    	{
    		if( this.id == u.getID() )
    			continue;
    		
    		double dir = u.getAngle();
    		result[0] += u.getX() + Math.cos(dir) * 32.0;
    		result[1] += u.getY() + Math.sin(dir) * 32.0;
    	}
    	result[0] /= (units.size() - 1);
    	result[1] /= (units.size() - 1);
    	
    	double dir = unit.getAngle();
    	result[0] -= unit.getX() + Math.cos(dir) * 32.0;
		result[1] -= unit.getY() + Math.sin(dir) * 32.0;
    	
    	return result;
    }
    
    private List<Unit> getNeighbors( double range )
    {
    	List<Unit> result = new ArrayList<Unit>();
    	
    	for( Unit u: bwapi.getMyUnits() )
    	{
    		if( unit.getID() == u.getID())
    			continue;
    		if( range < getDistance(u) )
    			continue;
    		
    		result.add(u);
    	}
    	
    	return result;
    }

    private double getDistance(Unit enemy) {
        int myX = unit.getX();
        int myY = unit.getY();

        int enemyX = enemy.getX();
        int enemyY = enemy.getY();

        int diffX = myX - enemyX;
        int diffY = myY - enemyY;

        double result = Math.pow(diffX, 2) + Math.pow(diffY, 2);

        return Math.sqrt(result);
    }

    public int getID() {
        return unit.getID();
    }
}
